Типы с плавающей точкой
=======================

.. index::
    Типы с плавающей точкой

В предыдущей главе мы видели, что, несмотря на простоту использования, арифметические
операции с целыми числами подвержены ошибкам программирования из-за переполнения и
усечения. Мы также видели, что целые числа не могут иметь значений с дробными
частями, как в 1.25.


Типы с плавающей запятой предназначены для поддержки чисел с дробной частью. "Точка" в
их названии происходит от точки основания, которая отделяет целую часть от
дробной, а "плавающая" относится к деталям реализации этих типов:
десятичная точка перемещается влево и вправо по мере необходимости. (Эта деталь
не важна при использовании этих типов.)

В этой главе мы также должны рассмотреть важные аспекты использования типов с плавающей точкой.
Прежде чем сделать это, я хотел бы привести список некоторых интересных аспектов типов с плавающей запятой:

#. Добавление 0,001 тысячу раз - это не то же самое, что добавление 1.
#. Прямое Использование логических операторов == и != с типами с плавающей запятой в большинстве случаев ошибочно.
#. Начальное значение типов с плавающей запятой равно .nan , а не 0. .nan не может использоваться в
   выражениях каким-либо значимым образом. При использовании в операциях сравнения
   значение .nan не меньше и не больше любого значения.
#. Для обозначения переполнения используются значения- .infinity и отрицательное .infinity .


Хотя типы с плавающей запятой в некоторых случаях более полезны, у них есть
особенности, которые должен знать каждый программист. По сравнению с целыми числами они
очень хорошо позволяют избежать усечения, поскольку их основное назначение - поддерживать
дробные значения. Как и любой другой тип, основанный на определенном количестве битов,
они тоже подвержены переполнению, но по сравнению с целыми числами диапазон значений, которые
они могут поддерживать, огромен. Кроме того, вместо того, чтобы демонстрировать молчаливое поведение в случае
переполнения, они получают специальные значения положительной и отрицательной бесконечности.
Напомним, что типы с плавающей запятой следующие:

.. index::
    float
    double
    real

.. list-table:: Title
   :widths: 25 25 50
   :header-rows: 1
   :align: center

   * - тип
     - диапазон
     - начальное значение
   * - float
     - знаковый 32 bits
     - float.nan
   * - double
     - знаковый 64 бит
     - double.nan
   * - real
     - минимум 64 бита, может быть больше, если позволяет аппарутуразнаковый  16 бит
     - real.nan

Свойства типов с плавающей точкой
---------------------------------

.. index::
    stringof
    sizeof
    max
    min
    dig

Типы с плавающей запятой обладают большим количеством свойств, чем другие типы:
* .stringof - это имя типа.
* .sizeof - длина типа в байтах. (Чтобы определить количество
битов, это значение необходимо умножить на 8, количество бит в байте.)
* .max - сокращение от "maximum" и является максимальным значением, которое может
иметь тип. Отдельного свойства .min для плавающих типов не существует; отрицательное значение
.max - это минимальное значение, которое может иметь тип. Например, минимальное
значение double равно -double.max .
* .min_normal - наименьшее положительное значение, которое может представлять этот тип с помощью
его нормальная точность. (Точность объясняется ниже.) Тип может представлять
меньшие значения, чем .min_normal, но эти значения не могут быть такими же точными, как
другие значения этого типа, и, как правило, вычисляются медленнее. Условие
значение с плавающей запятой, находящееся между отрицательным .min_normal и положительным
.min_normal (исключая 0), называется потерей точности.
* .dig - сокращение от "digits" и указывает количество цифр, которые обозначают
точность типа.
* .infinity - это специальное значение, используемое для обозначения переполнения.

Other properties of floating point types are used less commonly. You can see all of
them at Properties for Floating Point Types at dlang.org .
The properties of floating point types and their relations can be shown on a
number line like the following:

Другие свойства типов с плавающей запятой используются реже. Вы можете ознакомиться со всеми
ними в разделе Свойства типов с плавающей запятой по адресу .. _dlang.org:http://dlang.org/property.html.
Свойства типов с плавающей запятой и их взаимосвязи могут быть показаны в
числовой строке следующим образом:

 TODO  *вставить картинку*

За исключением двух специальных значений бесконечности, строка выше предназначена для масштабирования: количество
значений, которые могут быть представлены в диапазоне от min_normal до 1, равно количеству
значений, которые могут быть представлены в диапазоне от 1 до max . Это означает, что
точность дробных частей значений, которые находятся между min_normal и 1
, очень высока. (То же самое верно и для отрицательной стороны.)

.nan
-----

.. index::
    nan

Мы уже видели, что это значение по умолчанию для переменных с плавающей запятой.
.nan также может появляться в результате бессмысленных выражений с плавающей запятой.
Например, все выражения с плавающей запятой в следующей программе генерируют
double.nan :

.. code-block:: d

    import std.stdio;

    void main()
    {
        double zero = 0;
        double infinity = double.infinity;
        writeln("any expression with nan: ", double.nan + 1);
        writeln("zero / zero : ", zero / zero);
        writeln("zero * infinity: ", zero * infinity);
        writeln("infinity / infinity: ", infinity / infinity);
        writeln("infinity - infinity: ", infinity - infinity);
    }


.nan бесполезен только потому, что он указывает на неинициализированное значение. Он также полезен
, потому что распространяется в ходе вычислений, что упрощает и ускоряет
обнаружение ошибок.


Инициализация значений с плавающей запятой
-------------------------------------------

.. index::
    Инициализация значений с плавающей запятой

Значения с плавающей запятой могут быть построены из целых значений без десятичной точки,
например 123, или созданы непосредственно с десятичной точкой, например 123.0.
Значения с плавающей запятой также могут быть заданы с помощью специального
синтаксиса с плавающей запятой, как в 1.23e+4 . Часть e+ в этом синтаксисе может быть прочитана как "умноженная на 10 в
степени". Согласно этому показанию, предыдущее значение равно "1,23 умножить на 10 в
степени 4", что совпадает с "1,23 умножить на 10 4", что, в свою очередь, совпадает с
1.23x10000, что равно 12300.

Если значение после e отрицательное, как в 5.67e-3 , то оно читается как "деленное на 10 в
степени". Соответственно, этот пример - "5,67, деленное на 10 3", что, в свою очередь,
совпадает с 5,67/1000, будучи равным 0,00567.Формат с плавающей запятой очевиден в выходных данных следующей программы,
которая выводит свойства трех типов с плавающей запятой:

.. code-block:: d

    import std.stdio;

    void main()
    {
        writeln("Type: ", float.stringof);
        writeln("Precision: ", float.dig);
        writeln("Minimum normalized value: ", float.min_normal);
        writeln("Minimum value: ", -float.max);
        writeln("Maximum value: ", float.max);
        writeln();
        writeln("Type: ", double.stringof);
        writeln("Precision: ", double.dig);
        writeln("Minimum normalized value: ", double.min_normal);
        writeln("Minimum value: ", -double.max);
        writeln("Maximum value: ", double.max);
        writeln();
        writeln("Type: ", real.stringof);
        writeln("Precision: ", real.dig);
        writeln("Minimum normalized value: ", real.min_normal);
        writeln("Minimum value: ", -real.max);
        writeln("Maximum value: ", real.max);
    }


Вывод программы представленной выше на моём окружение будет следующим. Сдедует иметь ввиду что представление чисел с плавающей точкой
может зависит от конкретной аппартной реализации, поэтому на пользователском окружении можно получить иной вывод.

.. code-block:: console

        Type: float
        Precision: 6
        Minimum normalized value: 1.17549e-38
        Minimum value: -3.40282e+38
        Maximum value: 3.40282e+38
        Type: double
        Precision: 15
        Minimum normalized value: 2.22507e-308
        Minimum value: -1.79769e+308
        Maximum value: 1.79769e+308
        Type: real
        Precision: 18
        Minimum normalized value: 3.3621e-4932
        Minimum value: -1.18973e+4932
        Maximum value: 1.18973e+4932

*Примечание: Хотя double и real имеют большую точность, чем float , writeln выводит все
значения с плавающей запятой с точностью до 6 цифр. (Точность объясняется ниже.)*

*Наблюдения*


Как вы помните из предыдущей главы, максимальное значение long
состоит из 20 цифр: 18,446,744,073,709,551,616. Это значение выглядит небольшим по сравнению
даже с самым маленьким типом с плавающей запятой: float может иметь значения в
диапазоне от 10 до 38, например, 340,282,000,000,000,000,000,000,000,000,000,000,000,000.
Максимальное значение real находится в диапазоне 10 4932, значение, содержащее более 4900 цифр!
В качестве еще одного наблюдения давайте посмотрим на минимальное значение, которое double может
представлять с точностью до 15 цифр:

0.000...(there are 300 more zeroes here)...0000222507385850720

Переполнение не игнорируется
----------------------------

.. index::
    Переполнение

Несмотря на возможность принимать очень большие значения, типы с плавающей запятой
также подвержены переполнению. Типы с плавающей запятой в этом отношении безопаснее целочисленных типов
, поскольку переполнение не игнорируется. Значения, которые переполняются с положительной
стороны, становятся .infinity , а значения, которые переполняются с отрицательной стороны, становятся
‑.infinity . Чтобы увидеть это, давайте увеличим значение .max на 10%. Поскольку значение
уже находится на максимуме, увеличение на 10% приведет к переполнению:

.. code-block:: d

    import std.stdio;

    void main()
    {
        real value = real.max;
        writeln("Before: ", value);
        // Multiplying by 1.1 is the same as adding 10%
        value *= 1.1;
        writeln("Added 10%: ", value);
        value /= 2;
        writeln("Divided in half: ", value);
    }


Так ка поизошло переполнение, значение стало равным real.infinity , и останется таковым даже после деления на 2

.. code-block:: console

    Before: 1.18973e+4932
    Added 10%: inf
    Divided in half: inf

Точность
----------

.. index::
    Точность

Точность - это понятие, с которым мы сталкиваемся в повседневной жизни, но о
котором мало говорим. Точность - это количество цифр, которое используется при указании значения.
Например, когда мы говорим, что третья часть из 100 равна 33, точность равна 2, потому что 33 имеет
2 цифры. Если значение указано более точно как 33.33, то точность равна 4 цифрам.
Количество битов, которое используется в представлении типа с плавающей точкой, влияет не только на его максимальное
значение, но и на его точность. Чем больше число битов использовано в представлении, тем более точными являются
значения.

При делении нет усечения дробной части
--------------------------------------

Как мы видели в предыдущей главе, целочисленное деление не может сохранить
дробную часть результата:

.. code-block:: d

    int first = 3;
    int second = 2;
    writeln(first / second);

Вывод:

.. code-block:: console

    1

Типы с плавающей точкой не имеют подобной проблемы, поскольку они спроектированы именно с целью сохранения дробной части

.. code-block:: d

    double first = 3;
    double second = 2;
    writeln(first / second);

.. code:: console

    1.5

Точность дробной части зависит от точности типа: real имеет
самую высокую точность, а float - самую низкую.

Какой тип следует использовать
------------------------------

Если нет особой причины не делать этого, вы можете выбрать double для
значений с плавающей запятой. float имеет низкую точность, но из-за того, что он меньше, чем другие типы, он
может быть полезен при ограничении памяти. С другой стороны, поскольку точность
real на некоторых аппаратных средствах выше, чем в два раза, это было бы предпочтительнее для
высокоточных вычислений.

Все значения не могут быть представлены
----------------------------------------

Мы не можем представлять определенные значения в нашей повседневной жизни. В десятичной системе счисления, которую
мы используем ежедневно, цифры перед десятичной точкой представляют единицы, десятки, сотни и т.д.,
а цифры после десятичной точки представляют десятые, сотые,
тысячные доли и т.д.
Если значение создано из комбинации этих значений, оно может быть представлено
точно. Например, поскольку значение 0.23 состоит из 2 десятых и 3 сотых
, оно представлено точно. С другой стороны, значение 1/3 не может быть точно
представлено в десятичной системе счисления, поскольку количество цифр всегда равно
недостаточно, независимо от того, сколько указано: 0.33333...
Ситуация очень похожа с типами с плавающей запятой. Поскольку эти типы
основаны на определенном количестве битов, они не могут точно представлять каждое значение.
Разница с двоичной системой, используемой компьютерами, заключается в том, что цифры
перед десятичной точкой - это единицы, двойки, четверки и т.д., а цифры после десятичной
точки - половины, четверти, восьмые и т.д. Могут быть точно представлены только те значения, которые являются точными
комбинациями этих цифр.
Значение, которое не может быть точно представлено в двоичной системе, используемой
компьютеры - это 0,1, как в 10 центах. Хотя это значение может быть точно представлено в
десятичной системе, его двоичное представление никогда не заканчивается и непрерывно
повторяет четыре цифры: 0,0001100110011... (Обратите внимание, что значение записывается в двоичной
системе, а не в десятичной.) Оно всегда является неточным на некотором уровне в зависимости от
точности используемого типа с плавающей запятой.
Следующая программа демонстрирует эту проблему. Значение переменной
увеличивается на 0,001 тысячу раз в цикле. Удивительно, но результат
не равен 1:

.. code-block:: d

    import std.stdio;

    void main()
    {
        float result = 0;
        // Adding 0.001 for a thousand times:
        int counter = 1;
        while (counter <= 1000)
        {
            result += 0.001;
            ++counter;
        }
        if (result == 1)
        {
            writeln("As expected: 1");
        }
        else
        {
            writeln("DIFFERENT: ", result);
        }
    }


Поскольку значение 0.001 не можеь быть представлено точно, будем иметь накопление ошибки на каждой итерации

.. code-block:: console

    DIFFERENT: 0.999991


*Note: счетчик переменных, указанный выше, является счетчиком цикла. Явно определять переменную для
этой цели не рекомендуется. Вместо этого распространенным подходом является использование
цикла foreach, который мы рассмотрим в следующей главе.*
(:ref:`цикл foreach<глава_29_start>`)

Неупорядоченность
-----------------

Операторы сравнения, которые мы рассмотрели с целыми числами, используются
и с типами с плавающей запятой. Однако, поскольку специальное значение .nan представляет
недопустимые значения с плавающей запятой, сравнение .nan с другими значениями не имеет смысла.
Например, не имеет смысла спрашивать, больше ли .nan или 1. По этой причине значения с плавающей запятой вводят другую концепцию сравнения:
неупорядоченность. Неупорядоченность означает, что по крайней мере одно из значений равно .nan .
В следующей таблице перечислены все операторы сравнения с плавающей запятой. Все они
являются бинарными операторами (что означает, что они принимают два операнда) и используются как в
left == right . Столбцы, содержащие false и true, являются результатами операций
сравнения.
Последний столбец указывает, имеет ли смысл операция, если одним из
операндов является .nan . Например, даже если результат выражения 1.2 <
real.nan равен false , этот результат не имеет смысла, поскольку одним из операндов является
real.nan . Результат обратного сравнения real.nan < 1.2
также приведет к false. Аббревиатура lhs расшифровывается как левая сторона, указывая на
выражение в левой части каждого оператора.

.. list-table:: Операции сравнения чисел с плавающей точкой
   :widths: 25 50 25 25 25 25 25
   :header-rows: 1
   :align: center

   * - Оператор
     - Знасение
     - Если левая часть больше
     - Если левая часть меньше
     - Если обе части равны
     - Если одна из частей .nan
     - Интерпретация если .nan
   * - ==
     - равно?
     - false
     - false
     - true
     - false
     - yes
   * - !=
     - не равно?
     - false
     - false
     - true
     - false
     - yes
   * - >
     - больше?
     - true
     - false
     - false
     - false
     - no
   * - >=
     - больше равно?
     - екгу
     - false
     - true
     - false
     - no
   * - <
     - меньше?
     - false
     - true
     - false
     - false
     - no
   * - <=
     - меньше равно?
     - false
     - true
     - true
     - false
     - no

Хотя имеет смысл использовать с .nan , оператор == всегда выдает значение false
при использовании со значением .nan. Это имеет место, даже если оба значения равны .nan :

.. code-block:: d

    import std.stdio;

    void main()
    {
        if (double.nan == double.nan)
        {
            writeln("equal");
        }
    }

Хотя можно было бы ожидать, что значение double.nan будет равно самому себе, результат
сравнения будет ложным :

.. code:: console

    not equal

*isNaN() для верификации является параметр .nan*

Как мы видели выше, невозможно использовать оператор == для определения
того, является ли значение переменной с плавающей запятой .nan :

.. code-block:: d

    if (variable == double.nan) // неправильное сравнение
    {
        // ...
    }

isNaN() функция, находящаяся в модуле the std.math module предназначена для опредения является ли параметр .nan

.. code-block:: d

    import std.math;
    // ...
    if (isNaN(variable)) // правильно
    {
    // ...
    }

Упражнения
----------

#. Вместо float используйте double (или real ) в приведенной выше программе, которая добавила
   0,001 тысячу раз:

.. code-block:: console

   double result = 0;

   Это упражнение демонстрирует, насколько ошибочными
   могут быть сравнения равенства с плавающей запятой.

#. Доработайте калькулятор из предыдущей главы, чтобы он поддерживал
   типы с плавающей запятой. Новый калькулятор должен работать более точно с учетом этого изменения.
   При использовании калькулятора вы можете вводить значения с плавающей запятой в различных
   форматах, таких как 1000, 1.23 и 1.23e4.

#. Напишите программу, которая считывает 5 значений с плавающей запятой из входных данных. Заставьте
   программу сначала дважды выводить каждое значение, а затем одну пятую каждого значения.
   Это упражнение является подготовкой к концепции массива в следующей главе. Если
   вы напишете эту программу с учетом того, что вы видели до сих пор, вам будет
   легче понимать массивы и вы будете лучше их ценить.
